iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0
自我挑戰組

跟著 Go 實戰聖經 一起自學 Go系列 第 6

DAY 6 Go 語言 值與指標 (pointers) 是什麼?

  • 分享至 

  • xImage
  •  

昨天介紹了算符 (operators) 及零值 (zero values) ,今天就先來了解值與指標 (pointers) 吧!在知道什麼是指標之前,可以先簡單了解一下 Go 語言中其中一個記憶體管理系統 堆疊(stack)

堆疊 (stack) 是什麼?

舉例來說:
一般我們要把一個值 (A) 當作參數傳給 function 處理時,Go 會先將這個值複製建成一個新的變數 (新的 A) ,如果今天 function 要對參數做更動,因為已經有先複製成 (新的 A) ,則不會影響原來的值。

優點:可以減少程式碼錯誤,每個參數都會有屬於自己獨一無二的記憶體位置。
缺點:因為每次都要複製 (新的 A),複製本身會佔用過多記憶體位置

為了節省記憶體位置,指標 (pointers) 就誕生啦!

指標 (pointers) 是什麼?

https://ithelp.ithome.com.tw/upload/images/20220921/20151035rsWtRGP59u.png
指標為了解決堆疊的問題,所以他並不會複製值,那指標究竟是什麼呢?我通常把他想像成如上圖 (有名字的手指頭),當你今天想在資料庫找一個值 (A) 時,手指會指出值 (A) 的位置,指標因為他不會複製值,所以不會佔用無謂的記憶體空間。
當為一個值建立指標後, Go 語言會把他放在一個叫做 堆積 (heap) 的記憶體空間,而若是這個堆積的空間有一段時間都沒有指標指向他時,就會被 Go 丟掉,也就是 Go 語言裡的 垃圾回收機制 (garbage collection) ,也可以說是 Go 語言的堆積記憶體空間內,自帶打掃阿姨,幫你定期清你沒用到的垃圾。

優點:讓 function 呼叫起來更清爽,且可以簡化程式碼
缺點:因為指標有複雜的垃圾回收機制,所以有時可能比原本的堆疊更 耗費 CPU

補充:
當對一個值建立指標後,不能同時使用堆疊來管理該值,因為魚與熊掌不可兼得(開玩笑的~),其實是因為堆疊會考慮到變數的作用範圍 (scope) ,但指標並不用。

建立指標 (pointers)

1.用 var 宣告變數,把型別設為指標

var <變數> *<型別> // 在型別前加上 * 就可以宣告一個指標變數

補充:
用此種方法宣告指標變數,初始值為 nil 。

2.用函式 new() 賦值

new() 函式可以 為型別取得記憶體位置、填入型別的零值,且傳回記憶體的指標

<變數> := new(<型別>)
var <變數> = new(<型別>)

3.用 & 取得既有變數的指標

<變數 1> := &<變數 2>

以下練習用上述三種方式,來建立指標,並試著印出來看看!

範例 1:

package main

import (
	"fmt"
)

func main(){
    var name *string // 宣告 name 指標變數,初始值為 nil
    age := new(int) // 宣告 age 指標變數,初始值為 0
    height := 160
    myHeight := &height // 用 & 取得既有變數 (height) 的指標

    fmt.Printf("name: %#v\n", name) // 用格式化輸出,型別+記憶體位置
    fmt.Printf("age: %#v\n", age)
    fmt.Printf("myHeight: %#v\n", myHeight)
}

範例 1(執行結果):

name: (*string)(nil)
age: (*int)(0xc000016098)
myHeight: (*int)(0xc0000160a0)

從指標取得其值

上面範例 1 我們使用 fmt.Printf() ,但卻只能印出指標變數的型別+記憶體位置,究竟要怎麼印出值呢?很簡單,只要在指標變數前加上 * 符號 ,就可以順利解除指標的參照 (dereference) 順利拿到他所指向的值啦!

以下根據範例 1 ,稍做修改,來拿拿看指標所指向的值:
範例 2:

package main

import (
	"fmt"
)

func main(){
    var name *string // 宣告 name 指標變數,初始值為 nil
    age := new(int)
    height := 160
    myHeight := &height

    fmt.Printf("name: %#v\n",*name) // 在指標變數前加上 * ,印出其值
    fmt.Printf("age: %#v\n",*age)
    fmt.Printf("myHeight: %#v\n",*myHeight)
}

範例 2(執行結果):

panic: runtime error: invalid memory address or nil pointer dereference

咦!怎麼噴錯了?別擔心,仔細看看錯誤訊息,他是在說指標指到一個 nil 值,其實這是一個常見的錯誤,我們解除指標參照時,不小心解到一個 nil 值,解決方法就是在解除參照前先檢查是否為 nil (如下)。

範例 2-1:

package main

import (
	"fmt"
)

func main(){
    var name *string // 宣告 name 指標變數,初始值為 nil
    age := new(int)
    height := 160
    myHeight := &height

    if name != nil {
    fmt.Printf("name: %#v\n",*name) // 因為初始值為 nil,所以 if 不成立,不會印出
    }
    if age != nil {
    fmt.Printf("age: %#v\n",*age)
    }
    if myHeight != nil {
    fmt.Printf("myHeight: %#v\n",*myHeight)
    }
}

範例 2-1(執行結果):

age: 0
myHeight: 160

指標在 function 內的運行

若是一般變數,在 function 內的變動,只有在 function 內有作用,不會影響外面的世界,但若是指標變數傳入 function 則會改變原始的變數。

範例 3:

package main

import (
	"fmt"
)

func addValue(count int) {
    count += 10
    fmt.Println("addValue:",count)
}

func addPoint(count *int) {
    *count += 20
    fmt.Println("addPoint:",*count)
}

func main(){
    var count int
    addValue(count)
    fmt.Println("addValue in main:",count)
    addPoint(&count)
    fmt.Println("addPoint in main:",count)
}

範例 3(執行結果):

addValue: 10 // 在 addValue 這個 function 裡, count 的值為 10
addValue in main: 0 // 一般變數,在 function 內的變動,只有在 function 內有作用,故在外面 count 的值為初始值 0
addPoint: 20 // 在 addPoint 這個 function 裡, count 的值為 20
addPoint in main: 20 // 指標變數傳入 function 則會改變原始的變數,故 count 的值變為 20

今天介紹了在 Go 語言中,堆疊 (stack)、值與指標 (pointers) ,相信大家在實際了解其中差異之後,應該會對於何時要用指標更有想法了吧!
那我們明天繼續來介紹常數 (constants) 、列舉 (enums) 與變數作用範圍 (scope),明天見~


上一篇
DAY 5 Go 語言 算符 (operators) 及零值 (zero values) 的介紹
下一篇
DAY 7 Go 語言 常數 (constants) 、列舉 (enums) 與變數作用範圍 (scope)
系列文
跟著 Go 實戰聖經 一起自學 Go30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言